namespace Jsl.FxCop
{
    using System;
    using System.Collections.Generic;
    using System.Drawing;
    using Microsoft.FxCop.Sdk;

    /// <summary>
    /// A utility class for working with IL instructions.
    /// </summary>
    public static class InstructionHelper
    {
        /// <summary>
        /// Enumerate method calls to a specific type and method.
        /// </summary>
        /// <param name="instructions">The instructions to process.</param>
        /// <param name="typeName">The full name of the type.</param>
        /// <param name="methodName">The name of the method.</param>
        /// <returns>A method call enumerator.</returns>
        public static IEnumerable<int> EnumerateMethodCalls(InstructionCollection instructions, string typeName, string methodName)
        {
            for (var i = 0; i < instructions.Count; i++)
            {
                var instruction = instructions[i];
                if (IsMethodCall(instruction.OpCode))
                {
                    var method = (Method)instruction.Value;
                    if (method.Name.Name == methodName)
                    {
                        if (method.DeclaringType.FullName == typeName)
                        {
                            yield return i;
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Gets the type of the specified node.
        /// </summary>
        /// <param name="node">The node to get the type of.</param>
        /// <returns>The type of the specified node.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily", Justification = "Only one cast is ever done per call of the method.")]
        public static TypeNode GetTypeNodeFromNode(Node node)
        {
            if (node == null)
            {
                throw new ArgumentNullException("node");
            }

            switch (node.NodeType)
            {
                case NodeType.InstanceInitializer:
                    return ((Method)node).DeclaringType;

                case NodeType.Method:
                    return ((Method)node).ReturnType;

                case NodeType.This:
                    return ((This)node).Type;

                case NodeType.Literal:
                    return ((Literal)node).Type;

                case NodeType.Local:
                    return ((Local)node).Type;

                case NodeType.Field:
                    return ((Field)node).Type;

                case NodeType.Parameter:
                    return ((Parameter)node).Type;

                default:
                    return null;
            }
        }

        /// <summary>
        /// Determines if the method is empty.
        /// </summary>
        /// <param name="method">The method to check.</param>
        /// <returns><b>true</b> if the method is empty.</returns>
        public static bool IsEmptyMethod(Method method)
        {
            if (method == null)
            {
                throw new ArgumentNullException("method");
            }

            foreach (var instruction in method.Instructions)
            {
                var operationCode = instruction.OpCode;
                if ((operationCode != OpCode._Locals) && (operationCode != OpCode.Nop) && (operationCode != OpCode.Ret))
                {
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        /// Determines whether the specified OpCode is a LdArg.
        /// </summary>
        /// <param name="operationCode">The OpCode.</param>
        /// <returns><c>true</c> if the specified OpCode is a LdArg.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ldarg", Justification = "The name is an OpCode.")]
        public static bool IsLdarg(OpCode operationCode)
        {
            return (operationCode == OpCode.Ldarg) || (operationCode == OpCode.Ldarg_0) || (operationCode == OpCode.Ldarg_1)
                || (operationCode == OpCode.Ldarg_2) || (operationCode == OpCode.Ldarg_3) || (operationCode == OpCode.Ldarg_S);
        }

        /// <summary>
        /// Determines whether the specified OpCode is a LdArgA.
        /// </summary>
        /// <param name="operationCode">The OpCode.</param>
        /// <returns><c>true</c> if the specified OpCode is a LdArgA.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ldarg", Justification = "The name is an OpCode.")]
        public static bool IsLdargA(OpCode operationCode)
        {
            return (operationCode == OpCode.Ldarga) || (operationCode == OpCode.Ldarga_S);
        }

        /// <summary>
        /// Determines whether the specified OpCode is a LdcI4.
        /// </summary>
        /// <param name="operationCode">The OpCode.</param>
        /// <returns><c>true</c> if the specified OpCode is a LdcI4.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ldc", Justification = "The name is an OpCode.")]
        public static bool IsLdcI4(OpCode operationCode)
        {
            return (operationCode == OpCode.Ldc_I4) || (operationCode == OpCode.Ldc_I4_0) || (operationCode == OpCode.Ldc_I4_1)
                || (operationCode == OpCode.Ldc_I4_2) || (operationCode == OpCode.Ldc_I4_3) || (operationCode == OpCode.Ldc_I4_4)
                || (operationCode == OpCode.Ldc_I4_5) || (operationCode == OpCode.Ldc_I4_6) || (operationCode == OpCode.Ldc_I4_7)
                || (operationCode == OpCode.Ldc_I4_8) || (operationCode == OpCode.Ldc_I4_M1) || (operationCode == OpCode.Ldc_I4_S);
        }

        /// <summary>
        /// Determines whether the specified OpCode is a Ldloc.
        /// </summary>
        /// <param name="operationCode">The OpCode.</param>
        /// <returns><c>true</c> if the specified OpCode is a Ldloc.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ldloc", Justification = "The name is an OpCode.")]
        public static bool IsLdloc(OpCode operationCode)
        {
            return (operationCode == OpCode.Ldloc) || (operationCode == OpCode.Ldloc_0) || (operationCode == OpCode.Ldloc_1)
                || (operationCode == OpCode.Ldloc_2) || (operationCode == OpCode.Ldloc_3) || (operationCode == OpCode.Ldloc_S);
        }

        /// <summary>
        /// Determines whether the specified OpCode is a LdlocA.
        /// </summary>
        /// <param name="operationCode">The OpCode.</param>
        /// <returns><c>true</c> if the specified OpCode is a LdlocA.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Ldloc", Justification = "The name is an OpCode.")]
        public static bool IsLdlocA(OpCode operationCode)
        {
            return (operationCode == OpCode.Ldloca) || (operationCode == OpCode.Ldloca_S);
        }

        /// <summary>
        /// Determines whether the specified OpCode is a Leave.
        /// </summary>
        /// <param name="operationCode">The OpCode.</param>
        /// <returns><c>true</c> if the specified OpCode is a Leave.</returns>
        public static bool IsLeave(OpCode operationCode)
        {
            return (operationCode == OpCode.Leave) || (operationCode == OpCode.Leave_S);
        }

        /// <summary>
        /// Determines whether the specified OpCode is a method call.
        /// </summary>
        /// <param name="operationCode">The OpCode.</param>
        /// <returns><c>true</c> if the specified OpCode is a method call.</returns>
        public static bool IsMethodCall(OpCode operationCode)
        {
            return (operationCode == OpCode.Call) || (operationCode == OpCode.Callvirt);
        }

        /// <summary>
        /// Tries the get a Boolean value.
        /// </summary>
        /// <param name="method">The method.</param>
        /// <param name="instructionIndex">Index of the instruction.</param>
        /// <param name="value">The Boolean value if found.</param>
        /// <param name="startOfValueIndex">Start index of the of value.</param>
        /// <returns><c>true</c> if the value was a Boolean.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By design.")]
        public static bool TryGetBoolean(Method method, int instructionIndex, out bool value, out int startOfValueIndex)
        {
            // A LdcI4 instruction should be the previous instruction.
            Instruction ldcI4Instruction;
            if (!InstructionHelper.TryGetPreviousInstruction(
                method,
                instructionIndex,
                out ldcI4Instruction,
                out startOfValueIndex))
            {
                value = false;
                return false;
            }

            if (!InstructionHelper.IsLdcI4(ldcI4Instruction.OpCode))
            {
                value = false;
                return false;
            }

            value = (int)ldcI4Instruction.Value != 0;
            return true;
        }

        /// <summary>
        /// Tries to get the constructor.
        /// </summary>
        /// <param name="method">The method.</param>
        /// <param name="instructionIndex">Index of the instruction.</param>
        /// <param name="constructorMethod">The constructor method.</param>
        /// <param name="startOfConstructor">The start of constructor.</param>
        /// <param name="isVBStyle"><c>true</c> if is VB style.</param>
        /// <returns><c>true</c> if the constructor was found.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By design.")]
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "3#", Justification = "By design.")]
        public static bool TryGetConstructor(
            Method method,
            int instructionIndex,
            out Method constructorMethod,
            out int startOfConstructor,
            out bool isVBStyle)
        {
            // Assume we won't find the constructor.
            constructorMethod = null;
            isVBStyle = false;

            // Try to get the previous instruction. This should either be a ldloc instruction for VB or a
            // newobj instruction for C#.
            Instruction instruction;
            if (!InstructionHelper.TryGetPreviousInstruction(
                method,
                instructionIndex,
                out instruction,
                out startOfConstructor))
            {
                return false;
            }

            // If this is a newobj instruction then this is C# and return the constructor.
            if (instruction.OpCode == OpCode.Newobj)
            {
                constructorMethod = (Method)instruction.Value;
                return true;
            }

            // If this isn't a ldloc then assume this isn't VB and fail.
            if (!InstructionHelper.IsLdloc(instruction.OpCode))
            {
                return false;
            }

            // Get the instruction before the ldloc.
            if (!InstructionHelper.TryGetPreviousInstruction(
                method,
                startOfConstructor,
                out instruction,
                out startOfConstructor))
            {
                return false;
            }

            // Fail if this isn't a call OpCode.
            if (instruction.OpCode != OpCode.Call)
            {
                return false;
            }

            // We found the "constructor" call used in VB so return it.
            constructorMethod = (Method)instruction.Value;
            isVBStyle = true;
            return true;
        }

        /// <summary>
        /// Tries the get an integer.
        /// </summary>
        /// <param name="method">The method.</param>
        /// <param name="instructionIndex">Index of the instruction.</param>
        /// <param name="value">The integer value.</param>
        /// <param name="startOfValueIndex">Start index of the of value.</param>
        /// <returns><c>true</c> if the value was a integer.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By design.")]
        public static bool TryGetInteger(Method method, int instructionIndex, out int value, out int startOfValueIndex)
        {
            // Assume we won't find anything.
            value = 0;

            // A LdcI4 instruction should be the previous instruction.
            Instruction ldcI4Instruction;
            if (!InstructionHelper.TryGetPreviousInstruction(
                method,
                instructionIndex,
                out ldcI4Instruction,
                out startOfValueIndex))
            {
                return false;
            }

            if (!InstructionHelper.IsLdcI4(ldcI4Instruction.OpCode))
            {
                return false;
            }

            value = (int)ldcI4Instruction.Value;
            return true;
        }

        /// <summary>
        /// Tries the next previous instruction.
        /// </summary>
        /// <param name="method">The method.</param>
        /// <param name="instructionIndex">Index of the instruction.</param>
        /// <param name="instruction">The instruction.</param>
        /// <param name="thisInstructionIndex">Index of the this instruction.</param>
        /// <returns><c>true</c> if there was a next instruction.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By design.")]
        public static bool TryGetNextInstruction(
            Method method,
            int instructionIndex,
            out Instruction instruction,
            out int thisInstructionIndex)
        {
            if (method == null)
            {
                throw new ArgumentNullException("method");
            }

            thisInstructionIndex = instructionIndex;
            while (true)
            {
                // Fail if we are out of instructions.
                thisInstructionIndex++;
                if (thisInstructionIndex >= method.Instructions.Count)
                {
                    instruction = null;
                    return false;
                }

                // Get the current instruction to look at.
                instruction = method.Instructions[thisInstructionIndex];

                // If its an ignorable instruction then skip it.
                if ((instruction.OpCode != OpCode.Nop) && (instruction.OpCode != OpCode._Locals))
                {
                    return true;
                }
            }
        }

        /// <summary>
        /// Tries the get point.
        /// </summary>
        /// <param name="method">The method.</param>
        /// <param name="instructionIndex">Index of the instruction.</param>
        /// <param name="value">The point value.</param>
        /// <param name="startOfValueIndex">Start index of the of value.</param>
        /// <returns><c>true</c> if the value was a Point.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By design.")]
        public static bool TryGetPoint(Method method, int instructionIndex, out Point value, out int startOfValueIndex)
        {
            // Set defaults so we don't have to set multiple times in failure cases.
            value = new Point();
            startOfValueIndex = 0;

            // Try to get the constructor.
            Method ctorMethod;
            int startOfCtorIndex;
            bool isVBStyle;
            if (!InstructionHelper.TryGetConstructor(
                method,
                instructionIndex,
                out ctorMethod,
                out startOfCtorIndex,
                out isVBStyle))
            {
                return false;
            }

            // The constructor should be for a Point.
            if (ctorMethod.FullName != WellKnownTypeNames.Point + ".#ctor(" + WellKnownTypeNames.Int32 + "," + WellKnownTypeNames.Int32 + ")")
            {
                return false;
            }

            // Try to get the two integer values.
            int firstValue;
            int secondValue;
            int startOfIntsIndex;
            if (!InstructionHelper.TryGetTwoIntegers(method, startOfCtorIndex, out firstValue, out secondValue, out startOfIntsIndex))
            {
                return false;
            }

            // If this is VB style then there should be a ldloc.a instruction we need to skip.
            // Also set the startOfValueIndex as appropriate.
            if (isVBStyle)
            {
                Instruction ldlocInstruction;
                if (!InstructionHelper.TryGetPreviousInstruction(
                    method,
                    startOfIntsIndex,
                    out ldlocInstruction,
                    out startOfValueIndex))
                {
                    return false;
                }

                if (!InstructionHelper.IsLdlocA(ldlocInstruction.OpCode))
                {
                    return false;
                }
            }
            else
            {
                startOfValueIndex = startOfIntsIndex;
            }

            // Build the Point and return success.
            value = new Point(firstValue, secondValue);
            return true;
        }

        /// <summary>
        /// Tries the get previous instruction.
        /// </summary>
        /// <param name="method">The method.</param>
        /// <param name="instructionIndex">Index of the instruction.</param>
        /// <param name="instruction">The instruction.</param>
        /// <param name="thisInstructionIndex">Index of the this instruction.</param>
        /// <returns><c>true</c> if there was a previous instruction.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By design.")]
        public static bool TryGetPreviousInstruction(
            Method method,
            int instructionIndex,
            out Instruction instruction,
            out int thisInstructionIndex)
        {
            if (method == null)
            {
                throw new ArgumentNullException("method");
            }

            thisInstructionIndex = instructionIndex;
            while (true)
            {
                // Fail if we are out of instructions.
                thisInstructionIndex--;
                if (thisInstructionIndex < 0)
                {
                    instruction = null;
                    return false;
                }

                // Get the current instruction to look at.
                instruction = method.Instructions[thisInstructionIndex];

                // If its a Nop then skip it.
                if (instruction.OpCode != OpCode.Nop)
                {
                    return true;
                }
            }
        }

        /// <summary>
        /// Tries the size of the get.
        /// </summary>
        /// <param name="method">The method.</param>
        /// <param name="instructionIndex">Index of the instruction.</param>
        /// <param name="value">The size value.</param>
        /// <param name="startOfValueIndex">Start index of the of value.</param>
        /// <returns><c>true</c> if the value was a Size.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By design.")]
        public static bool TryGetSize(Method method, int instructionIndex, out Size value, out int startOfValueIndex)
        {
            // Set defaults so we don't have to set multiple times in failure cases.
            value = new Size();
            startOfValueIndex = 0;

            // Try to get the constructor.
            Method ctorMethod;
            int startOfCtorIndex;
            bool isVBStyle;
            if (!InstructionHelper.TryGetConstructor(method, instructionIndex, out ctorMethod, out startOfCtorIndex, out isVBStyle))
            {
                return false;
            }

            // The constructor should be for a Point.
            if (ctorMethod.FullName != WellKnownTypeNames.Size + ".#ctor(" + WellKnownTypeNames.Int32 + "," + WellKnownTypeNames.Int32 + ")")
            {
                return false;
            }

            // Try to get the two integer values.
            int widthValue;
            int heightValue;
            int startOfIntsIndex;
            if (!InstructionHelper.TryGetTwoIntegers(method, startOfCtorIndex, out widthValue, out heightValue, out startOfIntsIndex))
            {
                return false;
            }

            // If this is VB style then there should be a ldloc.a instruction we need to skip.
            // Also set the startOfValueIndex as appropriate.
            if (isVBStyle)
            {
                Instruction ldlocInstruction;
                if (!InstructionHelper.TryGetPreviousInstruction(
                    method,
                    startOfIntsIndex,
                    out ldlocInstruction,
                    out startOfValueIndex))
                {
                    return false;
                }

                if (!InstructionHelper.IsLdlocA(ldlocInstruction.OpCode))
                {
                    return false;
                }
            }
            else
            {
                startOfValueIndex = startOfIntsIndex;
            }

            // Build the Size and return success.
            value = new Size(widthValue, heightValue);
            return true;
        }

        /// <summary>
        /// Tries the get SizeF.
        /// </summary>
        /// <param name="method">The method.</param>
        /// <param name="instructionIndex">Index of the instruction.</param>
        /// <param name="value">The SizeF value.</param>
        /// <param name="startOfValueIndex">Start index of the of value.</param>
        /// <returns><c>true</c> if the value was a SizeF.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By design.")]
        public static bool TryGetSizeF(Method method, int instructionIndex, out SizeF value, out int startOfValueIndex)
        {
            // Set defaults so we don't have to set multiple times in failure cases.
            value = new SizeF();
            startOfValueIndex = 0;

            // Try to get the constructor.
            Method ctorMethod;
            int startOfCtorIndex;
            bool isVBStyle;
            if (!InstructionHelper.TryGetConstructor(method, instructionIndex, out ctorMethod, out startOfCtorIndex, out isVBStyle))
            {
                return false;
            }

            // The constructor should be for a Point.
            if (ctorMethod.FullName != WellKnownTypeNames.SizeF + ".#ctor(" + WellKnownTypeNames.Single + "," + WellKnownTypeNames.Single + ")")
            {
                return false;
            }

            // Try to get the height instruction.
            Instruction heightInstruction;
            int startOfHeightInstruction;
            if (!InstructionHelper.TryGetPreviousInstruction(
                method,
                startOfCtorIndex,
                out heightInstruction,
                out startOfHeightInstruction))
            {
                return false;
            }

            // Try to get the width instruction.
            Instruction widthInstruction;
            int startOfWidthInstruction;
            if (!InstructionHelper.TryGetPreviousInstruction(
                method,
                startOfHeightInstruction,
                out widthInstruction,
                out startOfWidthInstruction))
            {
                return false;
            }

            // Both instructions should be ldc_r4's.
            if ((heightInstruction.OpCode != OpCode.Ldc_R4) || (widthInstruction.OpCode != OpCode.Ldc_R4))
            {
                return false;
            }

            // If this is VB style then there should be a ldloc.a instruction we need to skip.
            // Also set the startOfValueIndex as appropriate.
            if (isVBStyle)
            {
                Instruction ldlocInstruction;
                if (!InstructionHelper.TryGetPreviousInstruction(
                    method,
                    startOfWidthInstruction,
                    out ldlocInstruction,
                    out startOfValueIndex))
                {
                    return false;
                }

                if (!InstructionHelper.IsLdlocA(ldlocInstruction.OpCode))
                {
                    return false;
                }
            }
            else
            {
                startOfValueIndex = startOfWidthInstruction;
            }

            // Build the Size and return success.
            value = new SizeF((float)widthInstruction.Value, (float)heightInstruction.Value);
            return true;
        }

        /// <summary>
        /// Tries the get string.
        /// </summary>
        /// <param name="method">The method.</param>
        /// <param name="instructionIndex">Index of the instruction.</param>
        /// <param name="value">The string value.</param>
        /// <param name="startOfValueIndex">Start index of the of value.</param>
        /// <returns><c>true</c> if the value was a string.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By design.")]
        public static bool TryGetString(Method method, int instructionIndex, out string value, out int startOfValueIndex)
        {
            // Assume we won't find anything.
            value = null;

            // A LdStr instruction should be the previous instruction.
            Instruction ldstrInstruction;
            if (!InstructionHelper.TryGetPreviousInstruction(
                method,
                instructionIndex,
                out ldstrInstruction,
                out startOfValueIndex))
            {
                return false;
            }

            if (ldstrInstruction.OpCode != OpCode.Ldstr)
            {
                return false;
            }

            value = (string)ldstrInstruction.Value;
            return true;
        }

        /// <summary>
        /// Tries the get two integers.
        /// </summary>
        /// <param name="method">The method.</param>
        /// <param name="instructionIndex">Index of the instruction.</param>
        /// <param name="firstValue">The first integer.</param>
        /// <param name="secondValue">The second integer.</param>
        /// <param name="startOfValuesIndex">Start index of the of integers.</param>
        /// <returns><c>true</c> if the value was two integers.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "2#", Justification = "By design.")]
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1021:AvoidOutParameters", MessageId = "3#", Justification = "By design.")]
        public static bool TryGetTwoIntegers(
            Method method,
            int instructionIndex,
            out int firstValue,
            out int secondValue,
            out int startOfValuesIndex)
        {
            // Assume failure.
            firstValue = 0;
            secondValue = 0;
            startOfValuesIndex = 0;

            int startOfSecondIntInstruction;
            if (!InstructionHelper.TryGetInteger(method, instructionIndex, out secondValue, out startOfSecondIntInstruction))
            {
                return false;
            }

            if (!InstructionHelper.TryGetInteger(method, startOfSecondIntInstruction, out firstValue, out startOfValuesIndex))
            {
                return false;
            }

            return true;
        }
    }
}